package me.cabbit.digitone.support;

import java.util.Random;

/**
 * Simple noise generator. It's a simple version of
 * fBm, using middle point displacement. Just specify
 * the number of octaves you want and the gain in between
 * octaves. Then use the {@link #getSample(double)} function
 * to get a random number;
 * 
 * @author Arkaid
 */
public class NoiseGenerator {

	/** samples produce by the generator */
	private double [] samples;
	
	/** simple linear interpolation routine
	 * 
	 * @param a value one to interpolate
	 * @param b value two to interpolate
	 * @param start position where a lies
	 * @param end position where b lies
	 * @param pos position betwen a and b you want to interpolate
	 * @return the value between a and b linearly interpolated by pos 
	 */
	private double lerp (double a, double b, double start, double end, double pos)
	{
		double x = (pos - start) / (end - start);
		double c = (b - a) * x + a;
		
		return c;
	}
	
	public NoiseGenerator(int octaves, double gain)
	{
		this(octaves, gain, -1);
	}
	
	/**
	 * Constructor. Builds the samples based on the paramaters
	 * 
	 * @param octaves number of octaves to apply. The more octaves, the more detail the noise has
	 * @param gain how much to reduce the the amplitude in between octaves
	 * @param seed seed for the RNG
	 */
	public NoiseGenerator(int octaves, double gain, long seed)
	{
		//do not allow for negative octaves
		if (octaves < 0)
			throw new RuntimeException("Number of octaves must be a positive number");
		
		//calculate the amount of samples needed to apply all octaves
		int nSamples = 1 << octaves;
		
		//create the array
		this.samples = new double[nSamples + 1];
		
		//init random generator
		Random rnd; 
		if (seed < 0)
			rnd = new Random();
		else
			rnd = new Random(seed);
		
		//start with an amplitude same as gain
		double amplitude = gain;
		
		//frequency determines the start and end points
		//in which the middle point lies
		int freq = nSamples; 

		//set the first and last point in the series
		//this.samples[0] = rnd.nextDouble() * 2 - 1;
		//this.samples[nSamples] = rnd.nextDouble() * 2 - 1;
				
		//process each octave
		for (int i = 0; i < octaves; i++)
		{
			//take "freq" chunks and find the middle point
			for (int j = 0; j < nSamples; j+=freq)
			{
				//start, end and middle point
				int a = j;
				int b = j + freq;
				int c = (a + b) / 2;
				
				//the new middle point will be the linear
				//interpolated value of the two extremes
				//displaced a random value (damped by gain)
				this.samples[c] = lerp(this.samples[a], this.samples[b], a, b, c);
				this.samples[c] += (rnd.nextDouble() * 2 - 1) * amplitude;
			}
			
			//reduce the frequency to half the previous octave
			freq >>= 1;
			
			//reduce the amplitude
			amplitude *= gain;
		}
	}
	
	/**
	 * Gets a sampled value from the the generated noise. 
	 * 
	 * @param a normalized value (between 0 and 1) representing a point within the function 
	 * @return a noise value from the function
	 */
	public double getSample (double position)
	{
		//get the number of samples we generated
		double nSamples = (this.samples.length - 1);
		
		//obtain two indices within the array from the 
		//given position value
		int idx_a = (int) (nSamples * position);
		int idx_b = (idx_a + 1) % this.samples.length;
		
		//get the two samples
		double sample_a = this.samples[idx_a];
		double sample_b = this.samples[idx_b];
		
		//get a position between 0~1 representing
		//a position between a and b
		double subPos = nSamples * position - idx_a;
		
		//interpolate the two samples
		return sample_a * (1 - subPos) + sample_b * subPos;
	}
	
}
